package bytecode;

import java.nio.channels.FileChannel;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.io.FileNotFoundException;
import java.io.File;
import marcelvm.InterpreterContext;
import static bytecode.InstructionToken.*;

/**
 * Parser bytecodu
 * @author honzee
 */
public class BytecodeParser {

    private FileInputStream input;
    private FileChannel channel;
    private List<InterpreterContext> ic;
    private int actIC;

    public BytecodeParser(File file) throws FileNotFoundException {
        this.ic = new ArrayList<InterpreterContext>();
        this.actIC = -1;
        this.input = new FileInputStream(file);
        this.channel = input.getChannel();
    }

    /**
     * Nastavi vstup na vybranou pozici
     */
    private void setPosition(int position) throws IOException {
        channel.position(position);
    }

    /**
     * Vrati aktualni pozici v bytecodu
     */
    private int getPosition() throws IOException {
        return (int)channel.position();
    }

    /**
     * Vraci dalsi byte ze vstupniho souboru
     */
    private int nextByte() throws IOException {
        return input.read();
    }

    /**
     * Pokud narazime na integer, tato metoda ho precte a vrati
     * v podobe pole bytu
     */
    private byte[] readInt() throws IOException {
        byte[] str = new byte[4];
        input.read(str, 0, 4);
        //System.out.println("Int: " + byteArrayToInt(str));
        return str;
    }

    /**
     * Pokud narazime na string, tato metoda ho precte a vrati
     * v podobe pole bytu
     */
    private byte[] readString(int length) throws IOException {
        byte[] str = new byte[length];
        input.read(str, 0, length);
        //System.out.println("String: " + str);
        return str;
    }

    /**
     * Vraci dalsi instruction token ze vstupniho souboru
     */
    private InstructionToken nextInstructionToken() throws IOException {
        InstructionToken it = InstructionToken.getToken(nextByte());
        //System.out.println("Instruction: " + it.name());
        return it;
    }
    
    /**
     * Skoci na vybranou pozici v bytecodu
     */
    private void goTo(int position) throws IOException {
        channel.position(position);
    }

    /**
     * Vrati aktualni kontext
     */
    private InterpreterContext getIC() {
        return ic.get(actIC);
    }

    /**
     * Pridava do seznamu intepreteru dalsi interpreter
     * - operace reprezentuje prepnuti kontextu pri volani
     *   funkce
     */
    private void addIC(InterpreterContext i) {
        actIC++;
        ic.add(i);
    }

    /**
     * Odebira ze seznamu intepreteru interpreter
     * - operace reprezentuje prepnuti kontextu pri navratu
     *   z funkce
     */
    private void removeIC() {
        ic.remove(actIC);
        actIC--;
    }

    /**
     * Spusti proces interpretace
     */
    public void start() throws Exception {
        InstructionToken token;
        byte[] value;
        addIC(initMainInterpreterContext());
        boolean end = false;

        while (!end) {
            token = nextInstructionToken();
            switch (token) {
                case LDS:
                    value = readInt();
                    value = readString(byteArrayToInt(value));
                    getIC().lds(new String(value));
                    break;
                    
                case LDC:
                    value = readInt();
                    getIC().ldc(byteArrayToInt(value));
                    break;

                case STORE:
                    value = readInt();
                    getIC().store(byteArrayToInt(value));
                    break;

                case LOAD:
                    value = readInt();
                    getIC().load(byteArrayToInt(value));
                    break;

                case ASTORE:
                    value = readInt();
                    getIC().astore(byteArrayToInt(value));
                    break;

                case ALOAD:
                    value = readInt();
                    getIC().aload(byteArrayToInt(value));
                    break;

                case CONVERT:
                    getIC().convert();
                    break;

                case WRITE:
                    value = readInt();
                    getIC().write(byteArrayToInt(value));
                    break;

                case WRITELN:
                    value = readInt();
                    getIC().writeln(byteArrayToInt(value));
                    break;

                case IREAD:
                    getIC().iread();
                    break;

                case OF:
                    value = readInt();
                    getIC().of(byteArrayToInt(value));
                    break;

                case CF:
                    getIC().cf();
                    break;

                case IADD:
                    getIC().iadd();
                    break;

                case SADD:
                    getIC().sadd();
                    break;

                case ISUB:
                    getIC().isub();
                    break;

                case IMUL:
                    getIC().imul();
                    break;

                case IDIV:
                    getIC().idiv();
                    break;

                case NEWARRAY:
                    value = readInt();
                    getIC().newarray(byteArrayToInt(value));
                    break;

                case DEALLOC:
                    getIC().dealloc();
                    break;

                case IF:
                    boolean b = false;
                    switch (nextInstructionToken()) {
                        case EQ:
                            b = getIC().eq();
                            break;
                        case NE:
                            b = getIC().ne();
                            break;
                        case GE:
                            b = getIC().ge();
                            break;
                        case LE:
                            b = getIC().le();
                            break;
                        case GT:
                            b = getIC().gt();
                            break;
                        case LT:
                            b = getIC().lt();
                            break;

                    }
                    value = readInt();
                    if (b) {
                        goTo(byteArrayToInt(value));
                    }
                    break;

                case GOTO:
                    value = readInt();
                    goTo(byteArrayToInt(value));
                    break;

                case INVOKE:
                    value = readInt();
                    int position = byteArrayToInt(value);
                    value = readInt();
                    List<Integer> args = getIC().invoke(byteArrayToInt(value));
                    addIC(initNewInterpreterContext(position, args));
                    break;

                case IRETURN:
                    Integer returnValue = getIC().ireturn();
                    returnFromInterpreterContext(returnValue);
                    break;

                case RETURN:
                case END:
                    end = true;
                    break;

                default:
                    throw new Exception("Unknown instruction found before - "
                            + nextByte());
            }
        }
    }

    /**
     * Najde main, nacte velikost zasobniku a tabulky promennych
     * a inicializuje InterpreterContext
     */
    private InterpreterContext initMainInterpreterContext() throws Exception {

        // najdeme main
        InstructionToken token = nextInstructionToken();
        while (token != MAIN) {
            token = nextInstructionToken();
        }

        // nacteme velikost stacku a locals
        token = nextInstructionToken();
        if (token != STACK) {
            throw new Exception("Parser error - stack expected, found:" + token.toString());
        }
        Integer stackSize = byteArrayToInt(readInt());

        token = nextInstructionToken();
        if (token != LOCALS) {
            throw new Exception("Parser error - locals expected, found:" + token.toString());
        }
        Integer localsSize = byteArrayToInt(readInt());

        return new InterpreterContext(stackSize, localsSize, 0);
    }

    /**
     * Najde main, nacte velikost zasobniku a tabulky promennych
     * a inicializuje InterpreterContext
     */
    private InterpreterContext initNewInterpreterContext(int position,
            List<Integer> args) throws Exception {

        // ulozime aktualni pozici do aktualniho kontextu
        // a presuneme se na hledanou pozici
        getIC().setPosition(getPosition());
        setPosition(position);

        InstructionToken token;
        // nacteme velikost stacku a locals
        token = nextInstructionToken();
        if (token != STACK) {
            throw new Exception("Parser error - stack expected, found:" + token.toString());
        }
        Integer stackSize = byteArrayToInt(readInt());

        token = nextInstructionToken();
        if (token != LOCALS) {
            throw new Exception("Parser error - locals expected, found:" + token.toString());
        }
        Integer localsSize = byteArrayToInt(readInt());

        InterpreterContext icNew = new InterpreterContext(stackSize, localsSize, position);
        // ulozime args do locals table
        int pos = 0;
        for (int k = args.size()-1; k >= 0; k--) {
            icNew.ldc(args.get(k));
            icNew.store(pos);
            pos++;
        }

        return icNew;
    }

    /**
     * Navrati se k predchozimu kontextu
     */
    public void returnFromInterpreterContext(Integer returnValue) throws IOException {
        // odebereme aktualni kontext
        removeIC();
        // puvodnimu kontextu dame na vrchol zasobniku vracenou hodnotu
        getIC().ldc(returnValue);
        //System.out.println("Return val: " + returnValue);
        // nastavime puvodni pozici
        setPosition(getIC().getPosition());
    }

    /**
     * Prevede pole bytu na integer
     */
    public static int byteArrayToInt(byte [] b) {
        return (b[0] << 24)   
                + ((b[1] & 0xFF) << 16)
                + ((b[2] & 0xFF) << 8)
                + (b[3] & 0xFF);
    }
}
